package com.moralesce.gt.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import com.moralesce.gt.concurrency.TaskExecutor;
import com.moralesce.gt.concurrency.TaskExecutorFactory;
import com.moralesce.gt.math.Combination;
import com.moralesce.gt.math.Enumerations;
import com.moralesce.gt.model.Board;
import com.moralesce.gt.model.Card;
import com.moralesce.gt.model.Deck;
import com.moralesce.gt.model.Hand;
import com.moralesce.gt.model.HandType;
import com.moralesce.gt.model.StartingHand;

/**
 * Provides utility methods for Hand instances.
 * 
 * @author Brandon Grenier
 */
public final class HandUtil {

	private static final FastLogger log = FastLogger.newInstance(HandUtil.class);
	private static final TaskExecutor executor = TaskExecutorFactory.newInstance();

	private HandUtil() {

	}

	/**
	 * Groups the specified hands by hand type.
	 * 
	 * <p>
	 * This method groups the specified list of hands by hand type. The result
	 * produces an ordered map, with the hand type as the key and a list of
	 * hands with the given hand type as the value.
	 * </p>
	 * 
	 * <p>
	 * The map is guaranteed have exactly nine entries, one entry for each hand
	 * type. If there are no hands for a particular hand type, the entry value
	 * will contain an empty list.
	 * </p>
	 * 
	 * @param hands The hands to group.
	 * 
	 * @return A SortedMap<HandType, List<Hand>> with all hands in the specified
	 * list grouped by hand type.
	 */
	public static SortedMap<HandType, List<Hand>> groupHandsByType(List<Hand> hands) {
		SortedMap<HandType, List<Hand>> values = new TreeMap<HandType, List<Hand>>();
		if (hands == null || hands.isEmpty()) {
			return values;
		}
		for (HandType handType : HandType.values()) {
			values.put(handType, new ArrayList<Hand>());
		}
		for (Hand hand : hands) {
			values.get(hand.getType()).add(hand);
		}
		return values;
	}

	/**
	 * 
	 */
	public static List<HandProbabilityProfile> heroProfile(StartingHand startingHand, Board board) {
		List<HandProbabilityProfile> profiles = new ArrayList<HandProbabilityProfile>();
		SortedMap<HandType, List<Hand>> hands = strongestHandsForHero(startingHand, board);
		SortedMap<HandType, Double> probabilities = probabilitiesForHeroByHandType(startingHand, board);

		for (HandType handType : HandType.values()) {
			List<Hand> sorted = ListUtil.sort(hands.get(handType));
			profiles.add(new HandProbabilityProfile(handType, probabilities.get(handType), sorted));
		}

		return ListUtil.sort(profiles);
	}

	/**
	 * 
	 */
	public static List<HandProbabilityProfile> villainProfile(StartingHand startingHand, Board board) {
		List<HandProbabilityProfile> profiles = new ArrayList<HandProbabilityProfile>();
		SortedMap<HandType, List<Hand>> hands = strongestHandsForVillain(startingHand, board);
		SortedMap<HandType, Double> probabilities = probabilitiesForVillainByHandType(startingHand, board);

		for (HandType handType : HandType.values()) {
			List<Hand> sorted = ListUtil.sort(hands.get(handType));
			profiles.add(new HandProbabilityProfile(handType, probabilities.get(handType), sorted));
		}

		return ListUtil.sort(profiles);
	}

	/**
	 * Returns a map of the strongest hands that can be made with the given
	 * starting hand and board, grouped by hand type.
	 * 
	 * @param startingHand The starting hand.
	 * @param board The board.
	 * 
	 * @return A SortedMap<HandType, List<Hand>> of the strongest hands that can
	 * be made with the given starting hand and board.
	 */
	public static SortedMap<HandType, List<Hand>> strongestHandsForHero(StartingHand startingHand, Board board) {
		return strongestHands(startingHand, board, new Deck());
	}

	public static SortedMap<HandType, List<Hand>> strongestHandsForVillain(StartingHand startingHand, Board board) {
		SortedMap<HandType, Set<Hand>> values = new TreeMap<HandType, Set<Hand>>();
		for (HandType handType : HandType.values()) {
			values.put(handType, new HashSet<Hand>());
		}

		Deck deck = new Deck().remove(startingHand.getCards()).remove(board.getCards());

		List<List<Card>> villainStartingCards = new Combination<Card>(deck.getCards(), 2).list();
		for (List<Card> vc : villainStartingCards) {
			StartingHand sh = StartingHand.newInstance(vc);
			Map<HandType, List<Hand>> hs = strongestHands(sh, board, new Deck().remove(startingHand.getCards()).remove(board.getCards()));
			for (Map.Entry<HandType, List<Hand>> kv : hs.entrySet()) {
				values.get(kv.getKey()).addAll(kv.getValue());
			}
		}

		SortedMap<HandType, List<Hand>> rval = new TreeMap<HandType, List<Hand>>();
		for (HandType handType : HandType.values()) {
			rval.put(handType, new ArrayList<Hand>(values.get(handType)));
		}

		return rval;
	}

	/**
	 * Returns a map containing the probability of drawing each type of hand,
	 * given the specified starting hand and board.
	 * 
	 * @param startingHand The starting hand.
	 * @param board The board.
	 * 
	 * @return A SortedMap<HandType, Double> containing the probability of
	 * drawing each hand type, given the specified starting hand and board.
	 */
	public static SortedMap<HandType, Double> probabilitiesForHeroByHandType(final StartingHand startingHand, final Board board) {
		final SortedMap<HandType, Double> probabilities = new TreeMap<HandType, Double>();
		for (final HandType handType : HandType.values()) {
			Runnable task = new Runnable() {
				public void run() {
					probabilities.put(handType, probabilityOfHeroMakingHandType(handType, startingHand, board));
				}
			};
			executor.execute(task);
		}
		while (probabilities.size() != 9) {
			ConcurrencyUtil.pause(100);
		}
		return probabilities;
	}

	/**
	 * Returns a map containing the probability of drawing each type of hand,
	 * given the specified starting hand and board.
	 * 
	 * @param startingHand The starting hand.
	 * @param board The board.
	 * 
	 * @return A SortedMap<HandType, Double> containing the probability of
	 * drawing each hand type, given the specified starting hand and board.
	 */
	public static SortedMap<HandType, Double> probabilitiesForVillainByHandType(final StartingHand startingHand, final Board board) {
		final SortedMap<HandType, Double> probabilities = new TreeMap<HandType, Double>();
		for (final HandType handType : HandType.values()) {
			Runnable task = new Runnable() {
				public void run() {
					probabilities.put(handType, probabilityOfVillainMakingHandType(handType, startingHand, board));
				}
			};
			executor.execute(task);
		}
		while (probabilities.size() != 9) {
			ConcurrencyUtil.pause(100);
		}
		return probabilities;
	}

	/**
	 * Returns the probability of hero making the specifed hand type given the
	 * specified starting hand and board.
	 * 
	 * @param handType The hand type.
	 * @param startingHand The starting hand.
	 * @param board The board.
	 * 
	 * @return The probability of hero making the specified hand type given the
	 * specified starting hand and board.
	 */
	public static Double probabilityOfHeroMakingHandType(HandType handType, StartingHand startingHand, Board board) {
		return probabilityOMakingHandType(handType, startingHand, board, new Deck());
	}

	/**
	 * Returns the probability of villain making the specifed hand type given
	 * <em>hero's</em> specified starting hand and board.
	 * 
	 * @param handType The hand type.
	 * @param startingHand Your starting hand.
	 * @param board The board.
	 * 
	 * @return The probability of villain making the specified hand type given
	 * <em>hero's</em> specified starting hand and board.
	 */
	public static Double probabilityOfVillainMakingHandType(HandType handType, StartingHand startingHand, Board board) {
		if (board.getCards().size() < 3) {
			throw new IllegalStateException("Can only calculate probabilities after the flop.");
		}

		Deck deck = new Deck().remove(startingHand.getCards()).remove(board.getCards());

		Double probability = 0.0;
		List<List<Card>> villainStartingCards = new Combination<Card>(deck.getCards(), 2).list();
		for (List<Card> vc : villainStartingCards) {
			StartingHand sh = StartingHand.newInstance(vc);
			probability += probabilityOMakingHandType(handType, sh, board, new Deck().remove(startingHand.getCards()).remove(board.getCards()));
		}
		return probability / (double) villainStartingCards.size();
	}

	private static SortedMap<HandType, List<Hand>> strongestHands(StartingHand startingHand, Board board, Deck deck) {
		List<Hand> hands = Enumerations.exclusiveHands(startingHand, board, deck);

		SortedMap<HandType, List<Hand>> handsByType = new TreeMap<HandType, List<Hand>>();
		for (HandType handType : HandType.values()) {
			handsByType.put(handType, new ArrayList<Hand>());
		}

		Map<String, Hand> handsByName = new HashMap<String, Hand>();
		for (Hand hand : hands) {
			handsByName.put(hand.getName(), hand);
		}

		for (Map.Entry<String, Hand> entry : handsByName.entrySet()) {
			handsByType.get(entry.getValue().getType()).add(entry.getValue());
		}

		return handsByType;
	}

	private static Double probabilityOMakingHandType(HandType handType, StartingHand startingHand, Board board, Deck deck) {
		if (board.getCards().size() < 3) {
			throw new IllegalStateException("Can only calculate probabilities after the flop.");
		}
		log.debug("Calculating probability of making " + handType.getLabel().toLowerCase() + " with starting hand '" + startingHand.getValue() + "'");

		deck.remove(ListUtil.merge(startingHand.getCards(), board.getCards()));
		List<List<Card>> draws = new Combination<Card>(deck.getCards(), (5 - board.getCards().size())).list();

		int total = 0;
		for (List<Card> cards : draws) {
			List<Hand> hands = Enumerations.exclusiveHands(startingHand, Board.newInstance(ListUtil.merge(board.getCards(), cards)));
			if (containsHandType(handType, hands)) {
				total++;
			}
		}

		log.debug(draws.size() + " hand combinations found, of which " + total + " are " + handType.getLabel().toLowerCase());
		return ((double) total / draws.size()) * 100.00;
	}

	private static boolean containsHandType(HandType handType, List<Hand> hands) {
		for (Hand hand : hands) {
			if (hand.getType().equals(handType)) {
				return true;
			}
		}
		return false;
	}

}
